/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package gride.plugin.executor;

import gride.events.EventListener;
import gride.events.GrideEvents;
import gride.gui.GUIManager;
import gride.plugin.GridePlugin;
import gride.project.GrideScript;
import gride.project.GrideLibrary;
import gride.project.Project;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.PrintStream;
import java.net.URL;
import java.security.Permission;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;

/**
 *
 * @author lukasz
 */
public class ExecutorPlugin extends GridePlugin
{
    private ExecutorGUIComponent guiComponent;

    private JMenuItem runProjectItem;
    private JMenuItem runFileItem;
    private JMenuItem stopItem;
    private JMenuItem pauseItem;

    private Thread executingThread = null;
    private GrideScript executingFile = null;
    private boolean paused = false;

    // if this flag is set, every time an execution script is terminated by an
    // exception, the EXECUTION_EXCEPTION event is raised
    // NOTE: each thread stop sets this flag, so should be reset before any
    // termination that is not supposed to produce error message
    private boolean notifyOnExecutionException = true;


    /**
     *
     */
    public ExecutorPlugin()
    {
        super();

        guiComponent = new ExecutorGUIComponent(true);

        runProjectItem = new JMenuItem("Run Project");
        runProjectItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5,
                                                   InputEvent.SHIFT_DOWN_MASK));
        runProjectItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                if (projectManager.getCurrentMainScript() != null)
                    run(projectManager.getCurrentMainScript());
            }
        });
        runProjectItem.setEnabled(false);

        runFileItem = new JMenuItem("Run File");
        runFileItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0));
        runFileItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                run(projectManager.getActiveFile());
            }
        });
        
        stopItem = new JMenuItem("Stop");
        stopItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0));
        stopItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                stopExecution();
            }
        });
        
        pauseItem = new JMenuItem("Pause");
        pauseItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0));
        pauseItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                if (!paused)
                    pauseExecution();
                else
                    resumeExecution();
            }
        });
    }


    private void run(final GrideScript file)
    {
        // if something is being executed, we have to terminate it first
        stopExecution();

        // request that all files are saved
        eventManager.raiseEvent(GrideEvents.SAVE_ALL_FILES, null);

        // raise an event notifying about execution
        Object[] args = { file };
        eventManager.raiseEvent(GrideEvents.EXECUTING_FILE, args);

        executingThread = new Thread()
        {
            @Override
            public void run()
            {
                try
                {
                    // set a security manager which will prevent the thread from
                    // killing the whole IDE by calling System.exit
                    System.setSecurityManager(new SecurityManager()
                    {
                        @Override
                        public void checkPermission(Permission permission)
                        {
                            if (permission.getName().contains("exitVM"))
                            {
                                // death penalty for trying to kill the JVM
                                stopExecution();

                                throw new SecurityException("Executed script " +
                                                          "called System.exit");
                            }
                        }
                    });

                    // redirect the input/output of the thread to gui components
                    System.setOut(new
                                   PrintStream(guiComponent.getOutputStream()));
                    System.setIn(guiComponent.getInputStream());

                    executionStarted(file);

                    // create a class loader to load the script
                    ClassLoader parent = getClass().getClassLoader();
                    GroovyClassLoader loader = new GroovyClassLoader(parent);

                    Project project = projectManager.getCurrentProject();

                    // add the scripts directory to classpath
                    String source = project.getSourceDir().getAbsolutePath();
                    loader.addClasspath(source);

                    // add all project's libraries to classpath
                    for (GrideLibrary lib : project.getLibraries())
                        loader.addClasspath(lib.getFile().getAbsolutePath());

                    // load the groovy script
                    Class groovyClass = loader.parseClass(file.getSourcefile());

                    // instantiate and run the script
                    GroovyObject groovyObject = (GroovyObject)
                                                      groovyClass.newInstance();
                    Object[] args = {};
                    groovyObject.invokeMethod("run", args);
                }
                catch (Exception e)
                {
                    if (notifyOnExecutionException)
                    {                        
                        Object[] args = { e };
                        eventManager.raiseEvent(GrideEvents.EXECUTION_EXCEPTION,
                                                args);
                    }

                    notifyOnExecutionException = true;
                }

                // reset the security manager
                System.setSecurityManager(null);

                executionFinished();
            }
        };

        guiManager.focusOnComponent(guiComponent);
        executingThread.start();
    }


    private void stopExecution()
    {
        if (executingThread != null)
        {
            // resume execution of paused thread -
            // otherwise get a synchronization error
            if (paused)
                resumeExecution();

            // prevents the message Error: ThreadDeath from being displayed
            notifyOnExecutionException = false;
            executingThread.stop();
        }
    }

    private void pauseExecution()
    {
        if (executingThread != null && !paused)
        {
            executingThread.suspend();

            new PrintStream(guiComponent.getOutputStream()).println(
                 "\n***** PAUSED EXECUTION OF " + executingFile.getFullName() +
                 " *****\n");

            pauseItem.setText("Resume");

            paused = true;
        }
    }


    private void resumeExecution()
    {
        if (executingThread != null && paused)
        {
            new PrintStream(guiComponent.getOutputStream()).println(
                "\n***** RESUMED EXECUTION OF " + executingFile.getFullName() +
                " *****\n");

            executingThread.resume();

            pauseItem.setText("Pause");

            paused = false;
        }

    }


    private void executionStarted(GrideScript file)
    {
        // set the file being executed
        executingFile = file;

        // enable input
        guiComponent.setInputEnabled(true);

        // enable stop and pause menu entries, disable run
        runFileItem.setEnabled(false);
        stopItem.setEnabled(true);
        pauseItem.setEnabled(true);

        System.out.println("\n***** STARTED EXECUTING " +
                           file.getFullName() + " *****\n");
    }


    private void executionFinished()
    {
        System.out.println("\n***** FINISHED EXECUTING " +
                           executingFile.getFullName() + " *****\n");

        // disable input
        guiComponent.setInputEnabled(false);

        // disable stop and pause menu entries, enable run
        runFileItem.setEnabled(true);
        stopItem.setEnabled(false);
        pauseItem.setEnabled(false);
        pauseItem.setText("Pause"); // in case we stopped while paused
        paused = false;

        executingThread = null;
    }

    /**
     * 
     */
    @Override
    public void activate()
    {
        guiManager.addComponent(GUIManager.BOTTOM_PANE, guiComponent, "Run");

        runFileItem.setEnabled(projectManager.getActiveFile() != null);
        
        stopItem.setEnabled(false);
        pauseItem.setEnabled(false);

        guiManager.addMenuItem(runProjectItem, "Run");
        guiManager.addMenuItem(runFileItem, "Run");
        guiManager.addMenuItem(stopItem, "Run");
        guiManager.addMenuItem(pauseItem, "Run");
        
        
        final Object myself = this;

        eventManager.addListener(GrideEvents.ACTIVE_FILE_CHANGED,
                new EventListener()
        {
            public Object whoIsListening()
            {
                return myself;
            }

            public void eventRaised(String eventID, Object[] args)
            {
                if (executingThread == null)
                {
                    runFileItem.setEnabled(projectManager.getActiveFile()
                                                                       != null);
                }
            }
        });

        eventManager.addListener(GrideEvents.EXITING_GRIDE, new EventListener()
        {
            public Object whoIsListening()
            {
                return myself;
            }

            public void eventRaised(String eventID, Object[] args)
            {
                // stop the script currently executed, if any
                stopExecution();
            }
        });

        EventListener mainScriptChangeListerer = new EventListener()
        {
            public Object whoIsListening()
            {
                return myself;
            }

            public void eventRaised(String eventID, Object[] args)
            {
                if (projectManager.getCurrentMainScript() != null)
                    runProjectItem.setEnabled(true);
                else
                    runProjectItem.setEnabled(false);
            }
        };

        eventManager.addListener(GrideEvents.MAIN_SCRIPT_CHANGED,
                                 mainScriptChangeListerer);

        eventManager.addListener(GrideEvents.PROJECT_LOADED,
                                 mainScriptChangeListerer);

        eventManager.addListener(GrideEvents.PROJECT_CLOSED,
                                 mainScriptChangeListerer);

        eventManager.addListener(GrideEvents.PROJECT_FILE_EXCLUDED,
                                 mainScriptChangeListerer);

        active = true;
    }

    /**
     *
     */
    @Override
    public void deactivate() 
    {
        guiManager.removeComponent(guiComponent);

        guiManager.removeMenuItem(runFileItem);
        guiManager.removeMenuItem(stopItem);
        guiManager.removeMenuItem(pauseItem);
        guiManager.removeMenuItem(runProjectItem);

        active = false;
    }

}
